{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "03b42dcc-c644-45a6-8a6a-3b250c74cee7",
   "metadata": {},
   "source": [
    "## Langchain Expression Language（LCEL）快速入门\n",
    "\n",
    "LCEL 是 LangChain 中的一个重要概念，**LCEL是一种声明式的链式组合语言**。它提供了一种统一的接口，允许不同的组件（如 `retriever`, `prompt`, `llm` 等）可以通过统一的 `Runnable` 接口连接起来。每个 `Runnable` 组件都实现了相同的方法，如 `.invoke()`、`.stream()` 或 `.batch()`，这使得它们可以通过 `|` 操作符轻松连接。\n",
    "\n",
    "### LCEL 的优势\n",
    "\n",
    "LCEL使得从基本组件构建复杂链变得容易，并支持流式处理、并行处理和日志记录等开箱即用的功能。\n",
    "\n",
    "- **统一接口**: LCEL 通过 `Runnable` 接口将不同的组件统一起来，简化了复杂操作的实现。\n",
    "- **模块化**: 各个组件可以独立开发和测试，然后通过 LCEL 轻松集成。\n",
    "- **可扩展性**: LCEL 支持异步调用、批处理和流式处理，适应不同的应用场景。\n",
    "\n",
    "\n",
    "### 组件\n",
    "\n",
    "我们已学习的组件包括以下模块：\n",
    "\n",
    "#### 📃 Model I/O：\n",
    "\n",
    "这包括提示管理，提示优化，用于聊天模型和LLM的通用接口，以及处理模型输出的常见实用工具。\n",
    "\n",
    "#### 📚 Retrieval：\n",
    "\n",
    "检索增强生成涉及从各种来源加载数据，准备数据，然后在生成步骤中检索数据以供使用。\n",
    "\n",
    "#### 🤖 Agents：\n",
    "\n",
    "Agents 允许LLM自主完成任务。 Agents会决定采取哪些行动，然后执行该行动，并观察结果，并重复此过程直到任务完成。 LangChain为代理提供了标准接口、可选择的代理列表以及端到端代理示例。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2b55489-48a5-4c2e-9a6e-3f246f0881c6",
   "metadata": {},
   "source": [
    "### 使用 LCEL 实现 LLMChain（Prompt + LLM)\n",
    "\n",
    "#### Pipe 管道操作符\n",
    "\n",
    "我们使用LCEL的 `|` 操作符将这些不同组件拼接成一个单一链。\n",
    "\n",
    "**在这个链中，用户输入被传递到提示模板，然后提示模板的输出被传递到模型，接着模型的输出被传递到输出解析器。**\n",
    "\n",
    "```python\n",
    "chain = prompt | model | output_parser\n",
    "```\n",
    "\n",
    "竖线符号类似于Unix管道操作符，它将不同的组件链接在一起，将一个组件的输出作为下一个组件的输入。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "09242ff5-b5ff-4902-9de8-59e4af945bf2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'当然可以！这是一个关于程序员的笑话：\\n\\n有一天，一个程序员走进酒吧，点了一杯啤酒。酒保问他：“你为什么这么沮丧？”\\n\\n程序员叹了口气说：“我写了一个程序，它可以解决我生活中的所有问题。”\\n\\n酒保好奇地问：“那为什么你还这么沮丧？”\\n\\n程序员无奈地回答：“因为我忘了给它加一个‘退出’的按钮！”\\n\\n希望这个笑话能让你笑一笑！'"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "# 初始化 ChatOpenAI 模型，指定使用的模型为 'gpt-4o-mini'\n",
    "model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "\n",
    "# 创建一个聊天提示模板，设置模板内容为\"讲个关于 {topic} 的笑话吧\"\n",
    "prompt = ChatPromptTemplate.from_template(\"讲个关于 {topic} 的笑话吧\")\n",
    "\n",
    "# 初始化输出解析器，用于将模型的输出转换为字符串\n",
    "output_parser = StrOutputParser()\n",
    "\n",
    "# 构建一个处理链，先通过提示模板生成完整的输入，然后通过模型处理，最后解析输出\n",
    "chain = prompt | model | output_parser\n",
    "\n",
    "# 调用处理链，传入主题为\"程序员\"，生成关于程序员的笑话\n",
    "chain.invoke({\"topic\": \"程序员\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9f4cd1e-7d55-4b14-ae37-9f7d66b07892",
   "metadata": {},
   "source": [
    "## 核心概念：invoke 方法\n",
    "\n",
    "Langchain `invoke` 方法是 LCEL 设计中的重要方法，可以帮助开发者更高效地处理复杂任务，结合语言模型进行系统构建，实现不同数据源和API的接口对接。\n",
    "\n",
    "### 基础概念\n",
    "\n",
    "- 在Langchain中，invoke方法是所有LangChain表达式语言（LCEL）对象的通用同步调用方法。通过invoke方法，开发者可以直接调用LLM或ChatModel，简化了调用流程，提高了开发效率。\n",
    "- 与其他链式调用方法相比，invoke方法更加灵活便捷，可以直接对输入进行调用，而不需要额外的链式操作。相对于batch和stream等异步方法，invoke方法更适用于单一操作的执行。\n",
    "\n",
    "### 使用方式\n",
    "\n",
    "- 单次调用：通过invoke方法，开发者可以快速对单一操作进行执行，例如转换ChatMessage为Python字符串等简单操作，提升了代码的可读性和整洁度。\n",
    "- 复杂协作：Langchain的核心理念就是将语言模型作为协作工具，invoke方法可以很好地实现开发者与语言模型的高效互动，构建出处理复杂任务的系统，并对接不同的数据源和API接口。\n",
    "\n",
    "## invoke 与 Model I/O 的结合\n",
    "\n",
    "整个流程按照以下步骤进行：\n",
    "\n",
    "1. `Prompt` 组件接收用户输入 **{\"topic\": \"程序员\"}**，然后使用该 topic 构建 `PromptValue`\n",
    "1. `Model` 组件获取生成的提示，并传递给 GPT-3.5-Turbo 模型进行评估。从模型生成的输出是一个ChatMessage对象。\n",
    "1. 最后，`output_parser` 组件接收ChatMessage，并将其转换为Python字符串，在invoke方法中返回。\n",
    "\n",
    "### Prompt\n",
    "\n",
    "`prompt` 是 `BasePromptTemplate` 的实例，这意味着它接受模板变量的字典并生成一个`PromptValue`。 \n",
    "\n",
    "PromptValue是一个包装器(wrapper)，围绕完成的提示进行操作，可以传递给LLM（以字符串作为输入）或ChatModel（以消息序列作为输入）。 \n",
    "\n",
    "它可以与任何语言模型类型一起使用，因为它定义了生成`BaseMessages`和生成字符串的逻辑。\n",
    "\n",
    "```python\n",
    "from langchain import PromptTemplate\n",
    "\n",
    "# Prompt 非 LCEL 使用方法\n",
    "prompt_template = PromptTemplate.from_template(\n",
    "    \"讲个关于 {topic} 的笑话吧\"\n",
    ")\n",
    "\n",
    "# 使用 format 生成提示\n",
    "prompt = prompt_template.format(topic=\"程序员\")\n",
    "print(prompt)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0d4b0ead-76d5-43e1-ba40-590b20894c43",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatPromptValue(messages=[HumanMessage(content='讲个关于 程序员 的笑话吧')])"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 调用 Prompt 的 invoke 方法生成最终的提示词\n",
    "prompt_value = prompt.invoke({\"topic\": \"程序员\"})\n",
    "prompt_value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "7ea46f7e-67cb-44c5-a5c1-6311a2a75639",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[HumanMessage(content='讲个关于 程序员 的笑话吧')]"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 适用于 ChatModel 的 Message 格式\n",
    "prompt_value.to_messages()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7b6030b5-f19f-42ee-9405-0bc229bd3c7b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Human: 讲个关于 程序员 的笑话吧'"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 字符串格式\n",
    "prompt_value.to_string()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "781d7c57-fd05-44b2-9638-2923ff532e0e",
   "metadata": {},
   "source": [
    "### Model\n",
    "\n",
    "然后调用模型的 `invoke` 方法，将 `PromptValue` 传递给模型。\n",
    "\n",
    "我们使用的 `GPT-4o-mini` 模型是 ChatModel，invoke 方法将返回 BaseMessage。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c82e786d-52f1-4082-92b5-00f3029d5983",
   "metadata": {},
   "outputs": [],
   "source": [
    "message = model.invoke(prompt_value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2a6a7a85-c1da-4441-a1db-cab6d0bd32a0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='当然可以！下面这个笑话希望能让你笑一笑：\\n\\n有一天，一个程序员走进一家咖啡店，点了一杯咖啡。咖啡师问他：“要加糖吗？” \\n\\n程序员回答：“不，我不需要加糖，我只需要调试一下。”\\n\\n咖啡师困惑地问：“调试什么？”\\n\\n程序员说：“就是把咖啡调得更好喝。”\\n\\n咖啡师无奈地摇摇头：“那你得自己写代码了！”\\n\\n希望这个笑话能让你开心！', response_metadata={'token_usage': {'completion_tokens': 121, 'prompt_tokens': 18, 'total_tokens': 139}, 'model_name': 'gpt-4o-mini', 'system_fingerprint': 'fp_f33667828e', 'finish_reason': 'stop', 'logprobs': None}, id='run-88600587-3701-4abc-99ec-b1bc19e474ff-0')"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "message"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4e36bb7-c7b3-49f1-9e6e-3a04c2d39d46",
   "metadata": {},
   "source": [
    "如果我们使用的是 LLM 模型  `gpt-3.5-turbo-instruct`，invoke 方法就会返回字符串。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "7b56176b-65d6-41f4-8071-058593e47a2b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'\\n\\nAI: 程序员和产品经理一起坐在一起吃饭，程序员问产品经理：“你知道为什么程序员喜欢吃快餐吗？”产品经理摇摇头：“为什么？”程序员答道：“因为快餐有个优点，就是不用写菜谱。”'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_openai import OpenAI\n",
    "\n",
    "llm = OpenAI(model=\"gpt-3.5-turbo-instruct\")\n",
    "llm.invoke(prompt_value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de91e1e1-c670-4803-a7d6-6b05df266961",
   "metadata": {},
   "source": [
    "### Output Parser\n",
    "\n",
    "最后，我们将模型输出传递给 output_parser，它是一个 `BaseOutputParser` 示例，意味着它接受字符串或 BaseMessage 作为输入。\n",
    "\n",
    "本指南中使用的 `StrOutputParser` 示例将所有输入都转换为字符串格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "150ceaf0-fcc1-4756-8fca-ab9bf28ed94a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'当然可以！下面这个笑话希望能让你笑一笑：\\n\\n有一天，一个程序员走进一家咖啡店，点了一杯咖啡。咖啡师问他：“要加糖吗？” \\n\\n程序员回答：“不，我不需要加糖，我只需要调试一下。”\\n\\n咖啡师困惑地问：“调试什么？”\\n\\n程序员说：“就是把咖啡调得更好喝。”\\n\\n咖啡师无奈地摇摇头：“那你得自己写代码了！”\\n\\n希望这个笑话能让你开心！'"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# message 经过 StrOutputParser 处理，变为标准的字符串\n",
    "output_parser.invoke(message)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c5e1741-83a4-4312-be93-76523e2c9373",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "c02797bf-df69-4c7d-ba0a-35df38d53173",
   "metadata": {},
   "source": [
    "## Invoke 与 Retrieval 结合\n",
    "\n",
    "下面演示如何在经典的 RAG 场景中使用 invoke 方法。下面将使用`|`操作符实现更复杂的链式调用。\n",
    "\n",
    "```python\n",
    "chain = setup_and_retrieval | prompt | model | output_parser\n",
    "```\n",
    "为了解释这一点，我们首先可以看到上面的提示模板接受上下文和问题作为要替换在提示中的值。在构建提示模板之前，我们希望检索相关文件以及将它们包含在上下文中。\n",
    "\n",
    "作为初步步骤，我们已经设置了使用内存存储器的检索器，它可以根据查询检索文档。这也是一个可运行的组件，并且可以与其他组件链接在一起，但您也可以尝试单独运行它：\n",
    "\n",
    "整个流程如下：\n",
    "\n",
    "1. 首先创建一个包含两个条目(entries)的 `RunnableParallel` 对象 **setup_and_retrieval**。第一个条目`context`将包括检索器获取的文档结果。第二个条目`question`将包含用户原始问题。为了传递问题，我们使用`RunnablePassthrough`来复制这个条目。\n",
    "2. 将上一步中的字典提供给`Prompt`组件。然后，它接收用户输入（即问题）以及检索到的文档（即context），构建提示并输出`PromptValue`。\n",
    "3. `Model` 组件接受生成的提示，并传递给OpenAI `gpt-4o-mini` 模型进行评估。模型生成的输出是一个ChatMessage对象。\n",
    "4. 最后，`output_parser` 组件接收ChatMessage，并将其转换为Python字符串，在调用方法中返回该字符串。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "6aa1a415-c38e-44c3-bda1-7e50aa6f7b46",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "model = ChatOpenAI(model=\"gpt-4o-mini\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "188130e3-54d8-4efe-a40e-c0ab81f9e274",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/ubuntu/miniconda3/envs/langchain/lib/python3.10/site-packages/pydantic/_migration.py:283: UserWarning: `pydantic.error_wrappers:ValidationError` has been moved to `pydantic:ValidationError`.\n",
      "  warnings.warn(f'`{import_path}` has been moved to `{new_location}`.')\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'harrison在kensho工作。'"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 导入 LangChain 库的不同模块，包括向量存储、输出解析器、提示模板、并行运行器和 OpenAI 的嵌入模型\n",
    "from langchain_community.vectorstores import DocArrayInMemorySearch\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnableParallel, RunnablePassthrough\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "\n",
    "# 使用 DocArrayInMemorySearch 创建一个内存中的向量存储\n",
    "# 使用 OpenAIEmbeddings 为文本生成嵌入向量，文本为 \"harrison worked at kensho\" 和 \"bears like to eat honey\"\n",
    "vectorstore = DocArrayInMemorySearch.from_texts(\n",
    "    [\"harrison worked at kensho\", \"bears like to eat honey\"],\n",
    "    embedding=OpenAIEmbeddings(),\n",
    ")\n",
    "\n",
    "# 将向量存储转换为检索器\n",
    "retriever = vectorstore.as_retriever()\n",
    "\n",
    "# 创建一个聊天提示模板，用中文设置模板以便生成基于特定上下文和问题的完整输入\n",
    "template = \"\"\"根据以下上下文回答问题:\n",
    "{context}\n",
    "\n",
    "问题: {question}\n",
    "\"\"\"\n",
    "prompt = ChatPromptTemplate.from_template(template)\n",
    "\n",
    "# 初始化输出解析器，将模型输出转换为字符串\n",
    "output_parser = StrOutputParser()\n",
    "\n",
    "# 设置一个并行运行器，用于同时处理上下文检索和问题传递\n",
    "# 使用RunnableParallel来准备预期的输入，通过使用检索到的文档条目以及原始用户问题，\n",
    "# 利用文档搜索器 retriever 进行文档搜索，并使用 RunnablePassthrough 来传递用户的问题。\n",
    "setup_and_retrieval = RunnableParallel(\n",
    "    {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
    ")\n",
    "\n",
    "# 构建一个处理链，包括上下文和问题的设置、提示生成、模型调用和输出解析\n",
    "chain = setup_and_retrieval | prompt | model | output_parser\n",
    "\n",
    "# 调用处理链，传入问题\"where did harrison work?\"（需翻译为中文），并基于给定的文本上下文生成答案\n",
    "chain.invoke(\"harrison在哪里工作？\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93bcecfd-e726-4570-bea6-19cd5b8fbd15",
   "metadata": {},
   "source": [
    "#### 忽略警告提示：\n",
    "\n",
    "UserWarning: `pydantic.error_wrappers:ValidationError` has been moved to `pydantic:ValidationError`.\n",
    "\n",
    "原因：`The issue is with pydantic version, it's 2.0.0+ and not compatible with docarray.\n",
    "Instead it should be pydantic==1.10.9`\n",
    "\n",
    "参考：https://github.com/langchain-ai/langchain/issues/15394\n",
    "\n",
    "LangChain官方关于 Pydatic 兼容性的说明：https://python.langchain.com/v0.1/docs/guides/development/pydantic_compatibility/\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cb7027c6-d85a-4af7-b5ef-1949d88f082f",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "800ffa20-782d-4f83-85e7-4349f8121186",
   "metadata": {},
   "source": [
    "### Homework: 使用持久化存储的向量数据库替换 DocArrayInMemorySearch，重写 LCEL 版本的 RAG 示例"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "82210b4c-df52-4b7f-abf2-97211ba9a7ec",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
